
import java.net.Socket;
import java.io.*;
import java.util.Arrays;

final class PeerConnection implements Runnable
{

	Socket socket; // socket for the connection
	OutputStream os;	// socket's output stream
	InputStream is;	//socket's input stream
    int otherPeerID;
    int lastRequestedPiece;
    boolean isOtherPeerChoked;
    boolean amIChoked;	
	int whoConnected;

    // Constructor
	public PeerConnection(Socket socket, int whoConnected) throws Exception
	{
		this.socket = socket;
		this.whoConnected = whoConnected;
		this.isOtherPeerChoked = true;
		try
		{
			this.os = this.socket.getOutputStream();
			this.is = this.socket.getInputStream();
		}
		catch(Exception e){System.out.println(e); e.printStackTrace();}
	}

	// Implement the run() method of the Runnable interface
	public void run()
	{
		try
		{
			startConnection();
			while(true)
			{
				try
				{
					processPackets();
				}
				catch(Exception f)
				{
					System.out.println(f); // print the Exception
					f.printStackTrace();
					/*StackTraceElement[] excep = f.getStackTrace();

					try
					{
						FileWriter fName = new FileWriter("peer_"+PeerProcess.ThisPeerId+"_exception.txt");
						fName.write(f+"\n");
						for(int j=0; j < excep.length; j++)
							fName.write(excep[j].toString()+"\n");
						fName.close();
					}
					catch(Exception g)
					{
						g.printStackTrace();
					}*/
				}
				Thread.yield();
			}
		}
		catch (Exception f)
		{
			System.out.println(f); // print the Exception
			f.printStackTrace();
			/*StackTraceElement[] excep = f.getStackTrace();

			try
			{
				FileWriter fName = new FileWriter("peer_"+PeerProcess.ThisPeerId+"_exception1.txt");
				fName.write(f+"\n");
				for(int j=0; j < excep.length; j++)
					fName.write(excep[j].toString()+"\n");
				fName.close();
			}
			catch(Exception g)
			{
				g.printStackTrace();
			}*/
		}
	}

	private void startConnection() throws Exception
	{
		int bytesRead;
		//Send sendHandshake message
		this.sendHandshakeMessage();
		
		//wait for receiveHandhshake
		byte[] receiveHandshake = new byte[32];
		bytesRead = is.read(receiveHandshake, 0, 32);
		if(bytesRead != 32)
		{
			while(bytesRead < 32)
			{
				bytesRead += is.read(receiveHandshake, bytesRead, 32-bytesRead);
			}
		}
		
		PacketReader checkHandshake = new PacketReader(receiveHandshake, true);
		
		//Receives handshake and beings log & connection
		this.otherPeerID = checkHandshake.getPeerID();
		if(this.whoConnected == 0)//recevied the connection
			PeerProcess.logEntry("is connected from Peer " + this.otherPeerID);
		else
			PeerProcess.logEntry("makes a connection to Peer " + this.otherPeerID);
		
		//send bitfield
		this.sendBitfieldMessage();
	}
	
	private void processPackets() throws Exception
	{
		//Sends all have messages to the other peer
		this.sendAllHaveMessages();
		
		//if process has ended, do nothing else
		if(PeerProcess.hasEnded)
			return;

		//use this variable alot to ensure we read the entire input stream from the socket
		int bytesRead = 0;
		
		//Creates the byte[] to read a messageHeader
		byte[] messageHeader = new byte[5];
			
		//Check if time to send choke or unchoke message
		boolean nowChoked = isOtherPeerNowChoked();
		if(nowChoked != this.isOtherPeerChoked) //if changed choked/unchoke status
		{
			this.isOtherPeerChoked = nowChoked;	//update variable
			if(this.isOtherPeerChoked)
				this.sendChokeMessage();
			else
				this.sendUnchokeMessage();
		}
			
		//if there is no message Header in the buffer, do nothing else
		if(this.is.available() <5)
			return;
		
		//Packet in Queue, process it
		bytesRead = this.is.read(messageHeader, 0, messageHeader.length);
		
		//Reads the entire packet header
		while(bytesRead < 5)
		{
			bytesRead += this.is.read(messageHeader, bytesRead, 5-bytesRead);
		}
			
		//create PacketReader
		PacketReader receivedHeader = new PacketReader(messageHeader, false);
		
		//read the new packet
		int type = receivedHeader.getMessageType();
		int length = receivedHeader.getMessageLength();
		byte[] payload;
		payload = new byte[length];
		
		bytesRead = 0;
		bytesRead = this.is.read(payload, 0, payload.length);
		//reads the entire packet
		while(bytesRead < payload.length)
		{
			bytesRead += this.is.read(payload, bytesRead, payload.length-bytesRead);
		}

		//switch on packet type
		switch(type)
		{
			case 0:	//choke
				//update choke variable
				this.amIChoked = true;
				//writes into log that I got choked
				PeerProcess.logEntry("is choked by Peer " + this.otherPeerID);
				//checks to see I do not have last requested piece
				if(!PeerProcess.PeerBitmapList.peerHasPiece(PeerProcess.ThisPeerId, this.lastRequestedPiece))
					PeerProcess.PeerBitmapList.setBitmap(PeerProcess.ThisPeerId, this.lastRequestedPiece, 0);
				break;
			
			case 1:	//unchoke
				//update choke variable
				this.amIChoked = false;
				//writes into log that I got unchoked
				PeerProcess.logEntry("is unchoked by Peer " + this.otherPeerID);

				if(PeerProcess.PeerBitmapList.checkIfPieceRequested(PeerProcess.ThisPeerId, this.lastRequestedPiece))
				{
					this.sendRequestMessage(this.lastRequestedPiece);
				}
				//randomly requests a new piece (if interested)
				else if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
				{
					//Choose a random piece and send request message
					int index = PeerProcess.PeerBitmapList.getPiecetoRequest(this.otherPeerID);
					this.sendRequestMessage(index);
				}
				break;
			
			case 2:	//interested
				//writes into log that I got an interested message
				PeerProcess.logEntry("received the 'interested' message from Peer " + this.otherPeerID);
				//if not on interested list, add them
				if(!PeerProcess.InterestedNeighbors.contains(new Integer(this.otherPeerID)))
					PeerProcess.InterestedNeighbors.add(new Integer(this.otherPeerID));
				break;
				
			case 3:	//not interested
				//writes into log that I got not interested message
				PeerProcess.logEntry("received the 'not interested' message from Peer " + this.otherPeerID);
				//remove from interested list
				if(PeerProcess.InterestedNeighbors.contains(new Integer(this.otherPeerID)))
					PeerProcess.InterestedNeighbors.remove(new Integer(this.otherPeerID));
				break;
				
			case 4:	//have
				int pieceNum = fourByteToInt(payload); 
				//writes into log that I got have message
				if(pieceNum <= PeerProcess.NumberOfPieces)
				{
					PeerProcess.logEntry("received the 'have' message from Peer " + this.otherPeerID + " for the piece " + pieceNum);
					//update data structures
					PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, pieceNum);
					//if interested, send the interested message
					if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
						this.sendInterestedMessage();
					else
						this.sendNotInterestedMessage();
					
					if(PeerProcess.PeerBitmapList.checkIfAllPieces(this.otherPeerID) && PeerProcess.incompletePeers.contains(new Integer(this.otherPeerID)))
						PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
				}
				else if(pieceNum == (PeerProcess.NumberOfPieces+1))//if a complete file downloaded message
				{
					PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, -1);
					PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
				}
				//if everyone has the complete file message
				else if(pieceNum == (PeerProcess.NumberOfPieces+2))
				{
					for(int i = 0; i < PeerProcess.indexByID.size(); i++)
						PeerProcess.PeerBitmapList.setBitmap(PeerProcess.indexByID.get(i), -1);
					PeerProcess.incompletePeers.clear();
				}
				break;
				
			case 5:	//bitfield
				//store their bitfield in memory
				PeerProcess.PeerBitmapList.setBitmap(this.otherPeerID, payload);
				//if interested or not, send the message regardless
				if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
					this.sendInterestedMessage();
				else
					this.sendNotInterestedMessage();
				//if the peer has the file, remove him from incomplete peers list
				if(PeerProcess.PeerBitmapList.checkIfAllPieces(this.otherPeerID) && PeerProcess.incompletePeers.contains(new Integer(this.otherPeerID)))
					PeerProcess.incompletePeers.remove(new Integer(this.otherPeerID));
				break;
			
			case 6:	//request
				//Reads the packet
				PacketReader packet1 = new PacketReader(payload);
				int index21 = packet1.getIndex();   
				
				//PeerProcess.logEntry("has received a request message from peer: "+this.otherPeerID+" for piece "+index21);

				//if they are not choked, send the piece to them
				if(!this.isOtherPeerChoked)
				{
					byte[] data = PeerProcess.mySplitter.returnPiece(index21);
					this.sendPieceMessage(index21, data);
				}
				break;
			
			case 7:	//piece
				//update received count (for choking)
				this.incrementReceivedCount();
				
				//Read the piece index
				byte[] index2 = new byte[4];
				for(int i = 0; i < 4; i++)
					index2[i] = payload[i];
				
				//Read the payload of the packet (minus the piece index)
				byte[] file = new byte[payload.length-4];
				for(int i = 4; i < payload.length; i++)
					file[i-4] = payload[i];
					
				//Make into a packetReader
				PacketReader packet = new PacketReader(index2, file);
				int pieceIndex = packet.getIndex();	//index of piece received
				
				//Add have message to send to everyone
				this.addHaveMessage(pieceIndex);
				
				//To store the piece and update the bitmap
				int idx = pieceIndex;
				PeerProcess.mySplitter.storePiece(idx, file);
				PeerProcess.PeerBitmapList.setBitmap(PeerProcess.ThisPeerId, idx, 1);

				//writes into log that I got a piece
				int totalPieces = PeerProcess.PeerBitmapList.returnNumberOfPieces();//GET THIS FROM BITMAPS
				PeerProcess.logEntry("has downloaded the piece " + pieceIndex + " from Peer " + this.otherPeerID + ". Now the number of pieces it has is "+totalPieces);
				
				if(totalPieces == PeerProcess.NumberOfPieces)
				{
						PeerProcess.logEntry("has downloaded the complete file");
						this.addHaveMessage(PeerProcess.NumberOfPieces+1);
				}

				//Requests a new piece or sends a not interested message
				if(!PeerProcess.PeerBitmapList.checkIfAllPieces(PeerProcess.ThisPeerId))
				{
					if(PeerProcess.PeerBitmapList.checkIfInterested(this.otherPeerID))
					{
						if(idx == this.lastRequestedPiece) //if not the last request piece, dont send a new request
						{
							//Choose a random piece and send request message
							int index = PeerProcess.PeerBitmapList.getPiecetoRequest(this.otherPeerID);
							this.sendRequestMessage(index);
						}
					}
					else
						this.sendNotInterestedMessage();
				}
				else //if I have all the pieces
				{
					this.sendNotInterestedMessage();
					if(PeerProcess.incompletePeers.contains(new Integer(PeerProcess.ThisPeerId)))
						PeerProcess.incompletePeers.remove(new Integer(PeerProcess.ThisPeerId));
				}				
				break;
				
			default:
				break;
		}
					
	}// end processPackets
	
	private void sendPacket(byte[] data) throws Exception
	{
		//Sending data
		this.os.write(data,0,data.length);
	    this.os.flush();
	}
	
	private void sendChokeMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(0);
		this.sendPacket(packet.getBytesToSend());	
	}
	
	private void sendUnchokeMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(1);
		this.sendPacket(packet.getBytesToSend());
	}
	
	private void sendInterestedMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(2);
		this.sendPacket(packet.getBytesToSend());
	}
	
	private void sendNotInterestedMessage() throws Exception
	{
		MessagePacket packet = new MessagePacket(3);
		this.sendPacket(packet.getBytesToSend());
	}
	
	private void sendHaveMessage(int index) throws Exception
	{
		MessagePacket packet = new MessagePacket(4, index);
		this.sendPacket(packet.getBytesToSend());
	}
	
	private void sendBitfieldMessage() throws Exception
	{
		byte[] bitmap = PeerProcess.PeerBitmapList.getByteArray(PeerProcess.ThisPeerId);
		MessagePacket packet = new MessagePacket(bitmap.length, 5, bitmap);
		this.sendPacket(packet.getBytesToSend());
	}
	
	private void sendRequestMessage(int index) throws Exception
	{
		if(!this.amIChoked)
		{
			MessagePacket packet = new MessagePacket(6, index);
			this.sendPacket(packet.getBytesToSend());
			this.lastRequestedPiece = index;
			//PeerProcess.logEntry("Sending Request to "+this.otherPeerID+" for piece "+index);
		}
	}
	
	private void sendPieceMessage(int index, byte[] piece) throws Exception
	{
		MessagePacket packet = new MessagePacket(piece.length+4, 7, index, piece);
		this.sendPacket(packet.getBytesToSend());
		//PeerProcess.logEntry("Sending piece "+index+" to "+this.otherPeerID);
	}
	
	private void sendHandshakeMessage() throws Exception
	{
		HandshakePacket sendHandshake = new HandshakePacket(Integer.toString(PeerProcess.ThisPeerId));
		this.sendPacket(sendHandshake.handshakeMessage());
	}
	
	private int fourByteToInt(byte[] bytes)
	{
		String temp = "";
		for(int i=0; i<4; i++)
		{
			if(bytes[i] != 0)
			{ 
				temp = temp + (char)(bytes[i]);
			}
		}
		//convert the byte[] into an int
		return Integer.parseInt(temp);
	}
	
	private void incrementReceivedCount()
	{
		int index = PeerProcess.indexByID.indexOf(new Integer(this.otherPeerID));
		int current = PeerProcess.receivedChunks.get(index);
		PeerProcess.receivedChunks.set(index, new Integer(current+1));
	}
	
	private void addHaveMessage(int pieceIndex)
	{
		int size = PeerProcess.indexByID.size();
		for(int i=0; i<size; i++)
		{
			PeerProcess.haveMsgsToTransmit.get(i).add(new Integer(pieceIndex));
		}
	}
	
	private void sendAllHaveMessages() throws Exception
	{
		int index = PeerProcess.indexByID.indexOf(new Integer(this.otherPeerID));
		int num = PeerProcess.haveMsgsToTransmit.get(index).size();
		for(int i=0; i<num; i++)
		{
			this.sendHaveMessage(PeerProcess.haveMsgsToTransmit.get(index).get(0));
			PeerProcess.haveMsgsToTransmit.get(index).remove(0);
		}
	}
	
	private boolean isOtherPeerNowChoked()
	{
		return !(PeerProcess.UnChokedNeighbors.contains(new Integer(this.otherPeerID)) || (PeerProcess.OptimisticUnchokedNeighbor==this.otherPeerID));
	}
	
}// end class PeerConnection
